Skip to content

[AC-1454] Migrate 2fa.directory call - #71

Open
lizard-boy wants to merge 16 commits into
mainfrom
ac1454-2fa-directory
Open

[AC-1454] Migrate 2fa.directory call#71
lizard-boy wants to merge 16 commits into
mainfrom
ac1454-2fa-directory

Conversation

@lizard-boy

@lizard-boy lizard-boy commented Oct 19, 2024

Copy link
Copy Markdown

Type of change

  • Bug fix
  • New feature development
  • Tech debt (refactoring, code cleanup, dependency upgrades, etc)
  • Build/deploy pipeline (DevOps)
  • Other

Objective

We currently call the 2fa.directory API every time a user generates the Inactive Two Factor report which isn't a very friendly approach. This PR now calls our backend instead. [Current settings] - we will cache the parsed results for 24 hours so we only hit the 2fa.directory API once/day.

Code changes

  • ReportsController.cs: Created Reports API controller with the future in mind of potentially moving around other report functions to the backend.
  • InactiveTwoFactorResponseModel.cs: Created response model containing a read only dictionary so the client doesn't have to do any further logic parsing in regards to the 2fa.directory data
  • GlobalSettings.cs: Added ITwoFactorDirectorySettings for potential customizations to the URL and cache expiration
  • ITwoFactorDirectorySettings.cs: Created settings object for use within GlobalSettings
  • TwoFactorDirectoryTotpResponseModel.cs: Created response model with desired fields for our flattened dictionary
  • GetInactiveTwoFactorQuery.cs: Created query to handle returning either a cached dictionary or a freshly parsed response from the 2fa.directory API. @r-tome created custom parser to handle the response json structure.
  • ToolsServiceCollectionExtensions.cs: Created collection extension to better organize future tools team specific services
  • ServiceCollectionExtensions.cs: Injected new tools services
  • test/*/GetInactiveTwoFactorQueryTests.cs: Created unit tests for the new query.

Shout-outs

Before you submit

  • Please check for formatting errors (dotnet format --verify-no-changes) (required)
  • If making database changes - make sure you also update Entity Framework queries and/or migrations
  • Please add unit tests where it makes sense to do so (encouraged but not required)
  • If this change requires a documentation update - notify the documentation team
  • If this change has particular deployment requirements - notify the DevOps team

Greptile Summary

This pull request implements a caching mechanism for the inactive two-factor authentication report, reducing API calls to the 2fa.directory service.

  • Added ReportsController in /src/Api/Tools/Controllers/ReportsController.cs to handle caching and retrieval of 2FA report data
  • Introduced GetInactiveTwoFactorQuery in /src/Core/Tools/Queries/GetInactiveTwoFactorQuery.cs for fetching and caching 2FA directory data
  • Created TwoFactorDirectorySettings in /src/Core/Settings/GlobalSettings.cs to manage API URL and cache expiration
  • Added unit tests in /test/Core.Test/Tools/Queries/GetInactiveTwoFactorQueryTests.cs to verify caching and API interaction
  • Implemented feature flag MigrateTwoFactorDirectory in /src/Core/Constants.cs for controlled rollout of the new functionality

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 file(s) reviewed, 13 comment(s)
Edit PR Review Bot Settings | Greptile

[Authorize("Application")]
public class ReportsController : Controller
{
private readonly ICurrentContext _currentContext;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: _currentContext is declared but never used

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@greptileai, can I remove this?

var user = await _userService.GetUserByPrincipalAsync(User);
if (!user.Premium)
{
throw new UnauthorizedAccessException("Premium required");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using a custom exception for premium requirements

{
public InactiveTwoFactorResponseModel() : base("inactive-two-factor") { }

public IReadOnlyDictionary<string, string> Services { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using init-only setter for immutability

@@ -0,0 +1,7 @@
namespace Bit.Core.Settings;

public interface ITwoFactorDirectorySettings

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation comments to describe the interface's purpose

[JsonPropertyName("documentation")]
public string Documentation { get; set; }
[JsonPropertyName("additional-domains")]
public IEnumerable<string> AdditionalDomains { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider using IReadOnlyCollection instead of IEnumerable for AdditionalDomains to prevent accidental modifications

Comment on lines +74 to +88
private static IEnumerable<TwoFactorDirectoryTotpResponseModel> ParseTwoFactorDirectoryTotpResponse(string json)
{
var data = new List<TwoFactorDirectoryTotpResponseModel>();
using var jsonDocument = JsonDocument.Parse(json);
// JSON response object opens with Array notation
if (jsonDocument.RootElement.ValueKind == JsonValueKind.Array)
{
// Each nested array has two values: a floating "name" value [index: 0] and an object with desired data [index: 1]
data.AddRange(from element in jsonDocument.RootElement.EnumerateArray()
where element.ValueKind == JsonValueKind.Array && element.GetArrayLength() == 2
select element[1].Deserialize<TwoFactorDirectoryTotpResponseModel>());
}

return data;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding error handling for JSON parsing exceptions


public interface IGetInactiveTwoFactorQuery
{
Task<Dictionary<string, string>> GetInactiveTwoFactorAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation comments to describe the method's purpose and return value.

Comment on lines +9 to +12
public static void AddToolsServices(this IServiceCollection services)
{
services.AddReportsQueries();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation comments to describe the purpose of this public method

sutProvider.GetDependency<IGlobalSettings>().TwoFactorDirectory.Returns(
new GlobalSettings.TwoFactorDirectorySettings()
{
CacheExpirationHours = 1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The cache expiration is set to 1 hour here, but the PR description mentions 24 hours. Ensure consistency across the implementation.

Uri = new Uri("http://localhost")
});

await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.GetInactiveTwoFactorAsync());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Throwing a BadRequestException for an Unauthorized response might not be the most appropriate exception. Consider using a more specific exception type.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This pull request implements server-side caching for the 2fa.directory API calls, reducing load on the external service and improving response times for the inactive two-factor authentication report.

  • Added distributed caching in GetInactiveTwoFactorQuery.cs with configurable 24-hour expiration
  • Implemented premium user validation in ReportsController.cs to restrict access to paid accounts
  • Created custom JSON parser in GetInactiveTwoFactorQuery.cs to handle the specific 2fa.directory response format
  • Added comprehensive unit tests covering cache hits, API failures, and successful API responses
  • Protected endpoint with feature flag MigrateTwoFactorDirectory for controlled rollout

12 file(s) reviewed, 10 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +43 to +44

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: style: Remove extra blank line before closing brace

{
public InactiveTwoFactorResponseModel() : base("inactive-two-factor") { }

public IReadOnlyDictionary<string, string> Services { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Property allows write access despite being IReadOnlyDictionary. Consider making the setter private to ensure immutability.

Comment thread src/Core/Constants.cs
/// flexible collections
/// </summary>
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string MigrateTwoFactorDirectory = "migrate-two-factor-directory";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding a documentation comment above this constant explaining its purpose, similar to other feature flags in this file

public class TwoFactorDirectorySettings : ITwoFactorDirectorySettings
{
public Uri Uri { get; set; } = new("https://api.2fa.directory/v3/totp.json");
public int CacheExpirationHours { get; set; } = 24;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: 24 hours may be too long for cache expiration - consider reducing to prevent stale data

Comment on lines +5 to +6
public Uri Uri { get; set; }
public int CacheExpirationHours { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making properties read-only to prevent unintended modifications after initialization

{
[Required]
[JsonPropertyName("domain")]
public string Domain { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Domain property should be initialized to avoid null reference exceptions since it's required

Comment on lines +10 to +12
public string Domain { get; set; }
[JsonPropertyName("documentation")]
public string Documentation { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider making properties init-only since they're only populated during deserialization

Comment on lines +41 to +43
using var client = _httpClientFactory.CreateClient();
var response =
await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, _globalSettings.TwoFactorDirectory.Uri));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding a timeout to the HTTP request to prevent hanging on slow responses

.Returns(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: style: Consider testing with actual 2FA directory response data instead of empty JSON to verify parsing logic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants